﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Text;
using System.Data.SqlClient;
using Djs.Neuronic.Core;
using Djs.Neuronic.Brain;

namespace Djs.Neuronic.Medium
{
    #region DatabaseManagerCls : Třída pro správu databáze
    /// <summary>
    /// DatabaseManagerCls : Třída pro správu databáze
    /// </summary>
    public class DatabaseManagerCls : IDisposable
    {
        public DatabaseManagerCls(CreatorCls creator, bool withDialog)
        {
            this.Creator = creator;
            this.WithDialog = withDialog;
            this.DatabaseDeclaration = creator.Database.Declarations.DatabaseDeclaration;
            this.DefaultDatabaseName = this.DatabaseDeclaration.DbDatabase.Name;
        }
        /// <summary>
        /// Odkaz na objekt Tvůrce, který je tichým pojítkem mezi všemi komponentami
        /// </summary>
        private CreatorCls Creator { get; set; }
        /// <summary>
        /// Deklarace databázových objektů.
        /// Autoinstanční property.
        /// </summary>
        private DatabaseDeclarationCls DatabaseDeclaration { get; set; }
        /// <summary>
        /// Výchozí jméno databáze
        /// </summary>
        private string DefaultDatabaseName { get; set; }
        /// <summary>
        /// Používat dialogy?
        /// </summary>
        private bool WithDialog { get; set; }
        void IDisposable.Dispose()
        {
            this.Creator = null;
        }
        /// <summary>
        /// Kontrola zadaného ConnectStringu, řešení problémů
        /// </summary>
        /// <param name="connectString"></param>
        /// <param name="showDialog"></param>
        /// <returns></returns>
        public string CheckConnectString(string connectString)
        {
            if (!String.IsNullOrEmpty(connectString))
                return this._CheckConnectString(connectString);
            else
                return this._SearchConnectStringBlind();
        }
        /// <summary>
        /// ConnectString je zadán, prověříme do jaké míry je použitelný.
        /// </summary>
        /// <param name="connectString"></param>
        /// <param name="showDialog"></param>
        /// <returns></returns>
        private string _CheckConnectString(string connectString)
        {
            DatabaseStatusCls dbStatus = DatabaseStatusCls.CreateForString(connectString);
            if (!dbStatus.ConnectStringReady || !dbStatus.ConnectionReady)
                // Conection nestojí za řeč: budeme hledat connection, jako by žádná nebyla zadaná:
                return this._SearchConnectStringBlind();
            if (!dbStatus.DatabaseExists || !dbStatus.HasSchemaBio)
                // Connection funguje, ale jmenovaná databáze neexistuje anebo neobsahuje schema "bio" (pak string ukazuje na cizí databázi): založíme novou databázi:
                return this._CreateNewDatabase(dbStatus.ConnectString, DefaultDatabaseName);
            // Databáze existuje, prověříme její stav:
            this._CheckDatabaseObjects(dbStatus);
            return null;
        }
        /// <summary>
        /// ConnectString není zadán (jde o prvotní spuštění, nebo zadaný string neukazuje ani na existující server, natož na DB).
        /// Pokusíme se najít funkční server (a případně i databázi, nebo si databázi vytvoříme).
        /// </summary>
        /// <returns></returns>
        private string _SearchConnectStringBlind()
        {
            List<string> dbConnStringList = new List<string>();
            List<DatabaseStatusCls> dbStatusList = new List<DatabaseStatusCls>();
            string[] serverNames = this._SearchSqlServerInstances().ToArray();  // new string[] { Environment.MachineName, @"DAJ-7-D\DAJ_7_D", "DJSDOMA", "localhost" };
            foreach (string serverName in serverNames)
                this._SearchConnectStringOnServer(serverName, dbConnStringList, dbStatusList);

            if (dbStatusList.Count > 0)
                return _SelectAndCheckFirstDatabase(dbStatusList);
            if (dbConnStringList.Count > 0)
                return _CreateDatabaseOnFirstServer(dbConnStringList);
            return null;
        }
        /// <summary>
        /// Vyhledá a vrátí seznam instancí SQL serverů dostupných z tohoto počítače.
        /// </summary>
        /// <returns></returns>
        private List<string> _SearchSqlServerInstances()
        {
            List<string> result = new List<string>();
            result.AddRange(_SearchSqlServerInstancesLocalWinReg());
            return result.Distinct().ToList();
        }
        /// <summary>
        /// Vyhledá a vrátí seznam instancí SQL serverů na this počítači, pomocí čtení WinReg.
        /// </summary>
        /// <returns></returns>
        private List<string> _SearchSqlServerInstancesLocalWinReg()
        {
            List<string> result = new List<string>();

            Microsoft.Win32.RegistryView registryView = Environment.Is64BitOperatingSystem ? Microsoft.Win32.RegistryView.Registry64 : Microsoft.Win32.RegistryView.Registry32;
            using (Microsoft.Win32.RegistryKey hklm = Microsoft.Win32.RegistryKey.OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine, registryView))
            {
                Microsoft.Win32.RegistryKey instanceKey = hklm.OpenSubKey(@"SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL", false);
                if (instanceKey != null)
                {
                    string prefix = Environment.MachineName + @"\";
                    foreach (var instanceName in instanceKey.GetValueNames())
                        result.Add(prefix + instanceName);
                }
            }
            return result;
        }
        /// <summary>
        /// Pokusí se připojit k danému serveru, a zjistit zda obsahuje databáze vhodné pro aplikaci.
        /// </summary>
        /// <param name="serverName"></param>
        /// <param name="dbConnStringList"></param>
        /// <param name="dbStatusList"></param>
        private void _SearchConnectStringOnServer(string serverName, List<string> dbConnStringList, List<DatabaseStatusCls> dbStatusList)
        {
            List<DatabaseStatusCls> result = new List<DatabaseStatusCls>();
            SqlConnectionStringBuilder csb = new SqlConnectionStringBuilder() { DataSource = serverName, IntegratedSecurity = true, ConnectTimeout = 4, InitialCatalog = "master" };
            using (DatabaseConnect connect = new DatabaseConnect(csb))
            {
                if (connect.Ready)
                {
                    dbConnStringList.Add(csb.ConnectionString);

                    List<DbElementDatabase> databases = DatabaseCls.DbInfoGetDatabases(connect, false);
                    foreach (DbElementDatabase database in databases)
                    {
                        DatabaseStatusCls dbStatus = DatabaseStatusCls.CreateForConnection(connect, database.Name);
                        if (dbStatus.HasSchemaBio || dbStatus.HasTableNeurons || dbStatus.HasTableSynapsis)
                            dbStatusList.Add(dbStatus);
                    }
                }
            }
        }
        /// <summary>
        /// Vybere první nejvhodnější databázi, a provede na ní údržbu a vrátí její ConnectString
        /// </summary>
        /// <param name="dbStatusList"></param>
        /// <returns></returns>
        private string _SelectAndCheckFirstDatabase(List<DatabaseStatusCls> dbStatusList)
        {
            if (dbStatusList.Count > 1)
                dbStatusList.Sort(DatabaseStatusCls.CompareBySuitability);
            DatabaseStatusCls dbStatus = dbStatusList[0];

            this._CheckDatabaseObjects(dbStatus);
            return dbStatus.ConnectString;
        }
        /// <summary>
        /// Na prvním serveru vytvoří novou databázi, vrátí její ConnectString.
        /// </summary>
        /// <param name="dbConnStringList"></param>
        /// <returns></returns>
        private string _CreateDatabaseOnFirstServer(List<string> dbConnStringList)
        {
            return this._CreateNewDatabase(dbConnStringList[0], DefaultDatabaseName);
        }
        /// <summary>
        /// Na SQL serveru daném connection vytvoří novou databázi a vrátí její connect string
        /// </summary>
        /// <param name="connectString"></param>
        /// <returns></returns>
        private string _CreateNewDatabase(string connectString, string databaseName)
        {
            DatabaseStatusCls dbStatus = null;

            DbElementDatabase database = this.DatabaseDeclaration.DbDatabase;
            using (DatabaseConnect connect = new DatabaseConnect(connectString, "master"))
            {
                if (!connect.Ready) return null;
                List<DbElementDatabase> databases = DatabaseCls.DbInfoGetDatabases(connect, true);
                string directory = this._DetectDataDirectoryFrom(databases);
                database.Create(connect, databaseName, directory);
                dbStatus = DatabaseStatusCls.CreateForConnection(connect, databaseName);
            }

            this._CheckDatabaseObjects(dbStatus);

            return dbStatus.ConnectString;
        }
        /// <summary>
        /// V dané databázi (která existuje) ověří její objekty a případně je doplní podle deklarace
        /// </summary>
        /// <param name="dbStatus"></param>
        private void _CheckDatabaseObjects(DatabaseStatusCls dbStatus)
        {
            using (DatabaseConnect connect = new DatabaseConnect(dbStatus.ConnectString))
            {
                DbElementDatabase dbDatabase = this.DatabaseDeclaration.DbDatabase;
                dbDatabase.Patch(connect);

                foreach (DbElementSchema dbSchema in this.DatabaseDeclaration.Schemas)
                    dbSchema.Patch(connect);

                foreach (DbElementTable dbTable in this.DatabaseDeclaration.Tables)
                    dbTable.Patch(connect);
            }
        }
        /// <summary>
        /// Vrátí adresář, do kterého se vytvoří soubory nové databáze
        /// </summary>
        /// <param name="databases"></param>
        /// <returns></returns>
        private string _DetectDataDirectoryFrom(List<DbElementDatabase> databases)
        {
            string file;

            // 1. Najdu první uživatelskou databázi na jiném disku než C:
            file = databases
                .Where(db => (db.IsUserName && db.DbFileMain.Substring(0, 1).ToUpper() != "C"))
                .Select(db => db.DbFileMain)
                .FirstOrDefault();
            if (file != null) return System.IO.Path.GetDirectoryName(file);

            // 2. Najdu první uživatelskou databázi uloženou kdekoliv:
            file = databases
                .Where(db => (db.IsUserName))
                .Select(db => db.DbFileMain)
                .FirstOrDefault();
            if (file != null) return System.IO.Path.GetDirectoryName(file);

            // 3. Najdu libovolnou databázi:
            file = databases
                .Select(db => db.DbFileMain)
                .FirstOrDefault();
            if (file != null) return System.IO.Path.GetDirectoryName(file);

            return null;
        }
        #region DatabaseStatusCls : Třída, která detekuje stav konkrétní databáze z pohledu aplikace Neuronic.
        /// <summary>
        /// Třída, která detekuje stav konkrétní databáze z pohledu aplikace Neuronic.
        /// Třída dokáže prověřit, zda databáze obsahuje klíčové objekty, a případně v jaké je verzi a kolik obsahuje dat.
        /// Tak je možno zvolit si nejvhodnější databázi k používání.
        /// Pokud bude použitelných více databází, může si mezi nimi uživatel vybírat.
        /// </summary>
        private class DatabaseStatusCls
        {
            private DatabaseStatusCls(string connectString)
            {
                this.ConnectString = connectString;
            }
            /// <summary>
            /// Vytvoří popis stavu databáze pro daný connectstring.
            /// Nikdy nevrací null.
            /// Nastavuje příznaky podle existence.
            /// </summary>
            /// <param name="connectString"></param>
            /// <returns></returns>
            public static DatabaseStatusCls CreateForString(string connectString)
            {
                DatabaseStatusCls dbStatus = new DatabaseStatusCls(connectString);
                if (String.IsNullOrEmpty(connectString)) return dbStatus;
                dbStatus.ConnectStringReady = true;

                SqlConnectionStringBuilder csb = new SqlConnectionStringBuilder(connectString);
                string databaseName = csb.InitialCatalog;
                csb.InitialCatalog = "master";
                using (DatabaseConnect connect = new DatabaseConnect(csb.ConnectionString))
                {
                    if (!connect.Ready) return dbStatus;
                    dbStatus.ConnectionReady = true;

                    DbElementDatabase dbDatabase = DatabaseCls.DbInfoGetDatabase(connect, databaseName);
                    dbStatus.DatabaseExists = (dbDatabase != null);
                    if (!dbStatus.DatabaseExists) return dbStatus;

                    connect.ChangeDatabase(databaseName);

                    DbElementSchema schemaBio = DatabaseCls.ReadRecord<DbElementSchema>(DbElementSchema.SqlSelectForName("bio"), connect);
                    dbStatus.HasSchemaBio = (schemaBio != null);

                    DbElementTable tableNeurons = DatabaseCls.ReadRecord<DbElementTable>(DbElementTable.SqlSelectForName("bio.neurons"), connect);
                    dbStatus.HasTableNeurons = (tableNeurons != null);

                    DbElementTable tableSynapsis = DatabaseCls.ReadRecord<DbElementTable>(DbElementTable.SqlSelectForName("bio.synapsis"), connect);
                    dbStatus.HasTableSynapsis = (tableSynapsis != null);
                }
                return dbStatus;
            }
            /// <summary>
            /// Vytvoří popis stavu databáze pro danou DatabaseConnect + jméno databáze.
            /// Nikdy nevrací null.
            /// Nastavuje příznaky podle existence.
            /// </summary>
            /// <param name="connectString"></param>
            /// <returns></returns>
            public static DatabaseStatusCls CreateForConnection(DatabaseConnect connect, string databaseName)
            {
                SqlConnectionStringBuilder csb = new SqlConnectionStringBuilder(connect.ConnectionString);
                csb.InitialCatalog = databaseName;

                DatabaseStatusCls dbStatus = new DatabaseStatusCls(csb.ConnectionString);
                dbStatus.ConnectStringReady = true;
                if (!connect.Ready) return dbStatus;
                dbStatus.ConnectionReady = true;

                dbStatus.DatabaseExists = true;

                DbElementSchema schemaBio = DatabaseCls.ReadRecord<DbElementSchema>(DbElementSchema.SqlSelectForName("bio"), connect);
                dbStatus.HasSchemaBio = (schemaBio != null);

                DbElementTable tableNeurons = DatabaseCls.ReadRecord<DbElementTable>(DbElementTable.SqlSelectForName("bio.neurons"), connect);
                dbStatus.HasTableNeurons = (tableNeurons != null);

                DbElementTable tableSynapsis = DatabaseCls.ReadRecord<DbElementTable>(DbElementTable.SqlSelectForName("bio.synapsis"), connect);
                dbStatus.HasTableSynapsis = (tableSynapsis != null);

                return dbStatus;
            }
            /// <summary>
            /// Zadaný Connect string
            /// </summary>
            public string ConnectString { get; private set; }
            /// <summary>
            /// true, pokud zadaný Connect string není prázdný
            /// </summary>
            public bool ConnectStringReady { get; private set; }
            /// <summary>
            /// true, pokud zadaný Connect string vede na SQL server, k němuž se lze přihlásit (neřeší existenci databáze)
            /// </summary>
            public bool ConnectionReady { get; private set; }
            /// <summary>
            /// true, pokud zadaná databáze existuje
            /// </summary>
            public bool DatabaseExists { get; private set; }
            /// <summary>
            /// true, pokud v zadané databázi existuje schema "bio"
            /// </summary>
            public bool HasSchemaBio { get; private set; }
            /// <summary>
            /// true, pokud v zadané databázi existuje tabulka "bio.neurons"
            /// </summary>
            public bool HasTableNeurons { get; private set; }
            /// <summary>
            /// true, pokud v zadané databázi existuje tabulka "bio.synapsis"
            /// </summary>
            public bool HasTableSynapsis { get; private set; }
            /// <summary>
            /// Vhodnost k použití: 0=nelze, 2=musí se doplnit schema, 5=musí se doplnit tabulky, 10=vše je OK
            /// </summary>
            private int Suitability
            {
                get
                {
                    if (!this.ConnectStringReady || !this.ConnectionReady || !this.DatabaseExists) return 0;
                    if (!this.HasSchemaBio) return 2;
                    if (!this.HasTableNeurons || !this.HasTableSynapsis) return 5;
                    return 10;
                }
            }
            /// <summary>
            /// Dovolí setřídit instance podle jejich vhodnosti. Na prvním místě seznamu bude nejvhodnější položka.
            /// </summary>
            /// <param name="a"></param>
            /// <param name="b"></param>
            /// <returns></returns>
            public static int CompareBySuitability(DatabaseStatusCls a, DatabaseStatusCls b)
            {
                return b.Suitability.CompareTo(a.Suitability);
            }
        }
        #endregion
    }
    #endregion
}
